Networks#

This tutorial provides a deep dive into PyPSA-GB’s network modeling, covering buses, lines, transformers, and different network resolution options.

What You’ll Learn#

  • Understanding network topology (buses, lines, transformers)

  • Different network models: Reduced, Zonal, ETYS

  • Network clustering for computational efficiency

  • Coordinate systems and bus mapping

  • ETYS network upgrades

Network Models in PyPSA-GB#

Model

Buses

Lines

Use Case

Reduced

32

~100

Fast testing, simplified analysis

Zonal

17

~40

Regional aggregation

ETYS

~2000

~3000

Full transmission detail

Clustered

50-200

Variable

Balance of detail and speed

1. Setup#

[1]:
import pypsa
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import warnings
import folium
from pyproj import Transformer
from _map_utils import prepare_map_network, explore_network_map

warnings.filterwarnings('ignore')
plt.style.use('seaborn-v0_8-whitegrid')
plt.rcParams['figure.figsize'] = [12, 8]
plt.rcParams['figure.dpi'] = 100

print(f"PyPSA version: {pypsa.__version__}")
PyPSA version: 1.0.7

2. Network Data Structure#

2.1 Load a Sample Network#

[2]:
# Load a reduced network for exploration
n = pypsa.Network("../../../resources/network/Historical_2015_reduced_solved.nc")

print("Network Components:")
print(f"  Buses: {len(n.buses)}")
print(f"  Lines: {len(n.lines)}")
print(f"  Transformers: {len(n.transformers)}")
print(f"  Links: {len(n.links)}")
print(f"  Generators: {len(n.generators)}")
print(f"  Loads: {len(n.loads)}")
print(f"  Storage Units: {len(n.storage_units)}")
INFO:pypsa.network.io:Imported network 'Historical_2015_reduced (Full)' has buses, carriers, generators, lines, links, loads, storage_units, sub_networks
Network Components:
  Buses: 32
  Lines: 99
  Transformers: 0
  Links: 3
  Generators: 1958
  Loads: 32
  Storage Units: 3

2.2 Buses#

Buses represent substations/nodes in the network. Key attributes:

  • x, y: Coordinates (OSGB36 meters for ETYS, WGS84 degrees for Reduced)

  • v_nom: Nominal voltage (kV)

  • carrier: Type (AC, DC)

[3]:
# Bus data
print("Bus DataFrame columns:")
print(n.buses.columns.tolist())
print(f"\nSample buses:")
n.buses.head(10)
Bus DataFrame columns:
['v_nom', 'type', 'x', 'y', 'carrier', 'unit', 'location', 'v_mag_pu_set', 'v_mag_pu_min', 'v_mag_pu_max', 'control', 'generator', 'sub_network', 'country']

Sample buses:
[3]:
v_nom type x y carrier unit location v_mag_pu_set v_mag_pu_min v_mag_pu_max control generator sub_network country
name
Beauly 275.0 248167.625895 845011.730526 AC 1.0 0.0 inf PQ 0 GB
Peterhead 275.0 411831.011338 843821.450890 AC 1.0 0.0 inf PQ 0 GB
Errochty 275.0 274353.767551 761097.048529 AC 1.0 0.0 inf PQ 0 GB
Denny/Bonnybridge 275.0 292803.094635 692060.857310 AC 1.0 0.0 inf PQ 0 GB
Neilston 400.0 248762.765458 659923.319765 AC 1.0 0.0 inf PQ 0 GB
Strathaven 400.0 282090.582169 652781.644926 AC 1.0 0.0 inf PQ 0 GB
Torness 400.0 368385.821758 670040.692176 AC 1.0 0.0 inf PQ 0 GB
Eccles 400.0 385644.869695 642664.271164 AC 1.0 0.0 inf PQ 0 GB
Harker 400.0 345770.517596 559939.870092 AC 1.0 0.0 inf PQ 0 GB
Stella West 400.0 421353.244668 565891.265585 AC 1.0 0.0 inf PQ 0 GB
[4]:
# Voltage levels
print("Buses by voltage level:")
if 'v_nom' in n.buses.columns:
    print(n.buses.groupby('v_nom').size())
Buses by voltage level:
v_nom
275.0     4
400.0    28
dtype: int64
[5]:
# Coordinate system detection
x_range = n.buses['x'].max() - n.buses['x'].min()
y_range = n.buses['y'].max() - n.buses['y'].min()

if x_range > 1000:
    coord_system = "OSGB36 (British National Grid - meters)"
else:
    coord_system = "WGS84 (Latitude/Longitude - degrees)"

print(f"Coordinate System: {coord_system}")
print(f"X range: {n.buses['x'].min():.2f} to {n.buses['x'].max():.2f}")
print(f"Y range: {n.buses['y'].min():.2f} to {n.buses['y'].max():.2f}")
Coordinate System: OSGB36 (British National Grid - meters)
X range: 128806.84 to 815766.92
Y range: 112990.04 to 845011.73

2.3 Lines#

Lines represent AC transmission circuits. Key attributes:

  • bus0, bus1: Connected buses

  • s_nom: Thermal rating (MVA)

  • x, r: Reactance and resistance (p.u.)

[6]:
print("Line DataFrame columns:")
print(n.lines.columns.tolist())
print(f"\nSample lines:")
n.lines[['bus0', 'bus1', 's_nom', 'x', 'r']].head(10)
Line DataFrame columns:
['bus0', 'bus1', 'type', 'x', 'r', 'g', 'b', 's_nom', 's_nom_mod', 's_nom_extendable', 's_nom_min', 's_nom_max', 's_nom_set', 's_max_pu', 'capital_cost', 'active', 'build_year', 'lifetime', 'length', 'carrier', 'terrain_factor', 'num_parallel', 'v_ang_min', 'v_ang_max', 'sub_network', 'x_pu', 'r_pu', 'g_pu', 'b_pu', 'x_pu_eff', 'r_pu_eff', 's_nom_opt', 'v_nom']

Sample lines:
[6]:
bus0 bus1 s_nom x r
name
0 Beauly Peterhead 525.0 0.0200 0.01220
1 Beauly Errochty 132.0 0.1500 0.00700
2 Beauly Peterhead 525.0 0.0200 0.01220
3 Beauly Errochty 132.0 0.1500 0.00700
4 Peterhead Denny/Bonnybridge 760.0 0.0650 0.00040
5 Peterhead Denny/Bonnybridge 760.0 0.0650 0.00040
6 Errochty Denny/Bonnybridge 648.0 0.0410 0.00300
7 Errochty Denny/Bonnybridge 648.0 0.0410 0.00300
8 Denny/Bonnybridge Torness 1090.0 0.0135 0.00211
9 Denny/Bonnybridge Strathaven 1500.0 0.0230 0.00130
[7]:
# Line capacity statistics
print("Line Capacity Statistics:")
print(f"  Total: {len(n.lines)} lines")
print(f"  Total capacity: {n.lines.s_nom.sum()/1000:.1f} GVA")
print(f"  Average rating: {n.lines.s_nom.mean():.0f} MVA")
print(f"  Max rating: {n.lines.s_nom.max():.0f} MVA")
print(f"  Min rating: {n.lines.s_nom.min():.0f} MVA")
Line Capacity Statistics:
  Total: 99 lines
  Total capacity: 230.8 GVA
  Average rating: 2331 MVA
  Max rating: 6960 MVA
  Min rating: 132 MVA

3. Network Visualization#

[9]:
# Interactive network map with WGS84 coordinates for the basemap
m = explore_network_map(
    n,
    map_style="light",
    tooltip=True,
    bus_size=50,
    bus_size_factor=2.0,
    branch_width_factor=2.0,
)
m

lon range: -6.09249939157784 4.058099999999997
lat range: 50.912041859956986 57.484467069924335
[9]:

4. Comparing Network Models#

4.1 Load Different Network Resolutions#

[10]:
# Try to load different network types
networks = {}

# Reduced
try:
    networks['Reduced (32)'] = pypsa.Network("../../../resources/network/Historical_2015_reduced_solved.nc")
    print(f"✓ Loaded Reduced network: {len(networks['Reduced (32)'].buses)} buses")
except FileNotFoundError:
    print("✗ Reduced network not found")

# ETYS (full or clustered)
try:
    networks['Clustered (~100)'] = pypsa.Network("../../../resources/network/HT35_clustered_solved.nc")
    print(f"✓ Loaded Clustered network: {len(networks['Clustered (~100)'].buses)} buses")
except FileNotFoundError:
    print("✗ Clustered network not found")

try:
    networks['Full ETYS'] = pypsa.Network("../../../resources/network/Historical_2023_etys_solved.nc")
    print(f"✓ Loaded Full ETYS network: {len(networks['Full ETYS'].buses)} buses")
except FileNotFoundError:
    print("✗ Full ETYS network not found")
INFO:pypsa.network.io:Imported network 'Historical_2015_reduced (Full)' has buses, carriers, generators, lines, links, loads, storage_units, sub_networks
✓ Loaded Reduced network: 32 buses
INFO:pypsa.network.io:Imported network 'HT35_clustered (Clustered)' has buses, carriers, generators, lines, links, loads, storage_units, stores, sub_networks
✓ Loaded Clustered network: 297 buses
INFO:pypsa.network.io:Imported network 'Historical_2023_etys (Full)' has buses, carriers, generators, lines, links, loads, storage_units, sub_networks, transformers
✓ Loaded Full ETYS network: 2044 buses
[11]:
# Compare network statistics
if len(networks) > 0:
    comparison = []
    for name, net in networks.items():
        comparison.append({
            'Network': name,
            'Buses': len(net.buses),
            'Lines': len(net.lines),
            'Transformers': len(net.transformers),
            'Links': len(net.links),
            'Generators': len(net.generators),
            'Total Line Cap (GVA)': net.lines.s_nom.sum()/1000
        })

    pd.DataFrame(comparison).set_index('Network')
[12]:
# Visual comparison of networks using interactive maps
if len(networks) >= 1:
    for name, net in networks.items():
        map_net = prepare_map_network(net)

        print(f"{name} ({len(net.buses)} buses, {len(net.lines)} lines)")
        display(
            map_net.plot.explore(
                map_style="light",
                tooltip=True,
                bus_size=50,
                bus_size_factor=2.0,
                branch_width_factor=2.0,
            )
        )

Reduced (32) (32 buses, 99 lines)
Clustered (~100) (297 buses, 499 lines)
Full ETYS (2044 buses, 1592 lines)

6. Coordinate Systems in PyPSA-GB#

ETYS uses OSGB36 (EPSG:27700)#

  • British National Grid

  • Coordinates in meters

  • X range: ~100,000 to 600,000

  • Y range: ~0 to 1,000,000

Reduced/Zonal use WGS84 (EPSG:4326)#

  • Latitude/Longitude

  • Coordinates in degrees

  • X (lon): -8 to 2

  • Y (lat): 49 to 61

[13]:
# Coordinate conversion example
try:
    from pyproj import Transformer

    # WGS84 to OSGB36
    transformer = Transformer.from_crs("EPSG:4326", "EPSG:27700", always_xy=True)

    # Example: London coordinates
    lon, lat = -0.1276, 51.5074
    x, y = transformer.transform(lon, lat)

    print("Coordinate Conversion Example (London):")
    print(f"  WGS84 (lon, lat): {lon}, {lat}")
    print(f"  OSGB36 (x, y): {x:.0f}, {y:.0f} meters")
except ImportError:
    print("pyproj not installed - coordinate conversion not available")
Coordinate Conversion Example (London):
  WGS84 (lon, lat): -0.1276, 51.5074
  OSGB36 (x, y): 530041, 180380 meters

7. Network Clustering#

PyPSA-GB supports clustering the full ETYS network for faster solving.

[14]:
# Clustering configuration example
clustering_config = """
# Spatial clustering to GSP regions
clustering:
  method: spatial
  config:
    boundaries_path: "data/network/GSP/GSP_regions.geojson"
    cluster_column: "GSPs"
    bus_crs: "EPSG:27700"
    boundary_crs: "EPSG:27700"

# K-means clustering
clustering:
  method: kmeans
  n_clusters: 100
"""
print("Clustering Configuration Options:")
print(clustering_config)
Clustering Configuration Options:

# Spatial clustering to GSP regions
clustering:
  method: spatial
  config:
    boundaries_path: "data/network/GSP/GSP_regions.geojson"
    cluster_column: "GSPs"
    bus_crs: "EPSG:27700"
    boundary_crs: "EPSG:27700"

# K-means clustering
clustering:
  method: kmeans
  n_clusters: 100